The Let's Play Archive

EXAPUNKS

by Carbon dioxide

Part 35: Last Stop Snaxnet - Nuclear centrifuges

Part 35 - Last Stop Snaxnet - Nuclear centrifuges

=== Trash World Inbox ===

My top scores for the modem dialing were 865 cycles and 38 size.

GuavaMoment brought the cycle count down to 852/77/9 by recognizing that the first three digits of the phone numbers (area code?) are always the same. This solution also has some slightly different file handling. -1 is written after a number it if still should be dialed and 0 if it has been dialed. This way old numbers actually get overwritten when a new one comes in. I'm not sure how much difference this makes because there's some jumping back and forth to check the value of that 0/-1 status digit.

GuavaMoment posted:

code:
GRAB 300
LINK 800
@REP 11
COPY F #DIAL
@END
REPL LINKS
WIPE
MAKE
VOID M
MARK PASTE
@REP 8
COPY M F
@END
COPY -1 F
SEEK 999
TEST MRD
TJMP PASTE
SEEK -54
ADDI X 1 X
COPY -1 #DIAL
MARK CALLED
SEEK 8
TEST F < 0
FJMP CALLED
SEEK -9
COPY 4 #DIAL
COPY 7 #DIAL
COPY 2 #DIAL
@REP 8
COPY F #DIAL
@END
REPL LINKS
COPY 0 F;INSTEAD OF NOP
TEST MRD
FJMP CALLED
VOID M
TEST X = 7
TJMP END
SEEK -9
JUMP PASTE

MARK LINKS
LINK 800
COPY 999 M
TEST X = 7
TJMP END
GRAB 200
MARK PASTING
SEEK 4
@REP 8
COPY F M
@END
JUMP PASTING

MARK END
WIPE
silentsnack went for a completely different low cycle solution.

silentsnack posted:

code:
;XA - GLOBAL
GRAB 300
LINK 800
JUMP START

MARK SUCCESS
MODE      ;LAZY HACK
VOID M    ;BUT IT WORKS?

MARK READER
GRAB 200
MARK READ
SEEK 1
COPY F M
@REP 5
MULI F 10 X
ADDI X F M
@END
JUMP READ

MARK MAIN_LOOP
ADDI X 11 X
GRAB 300
MARK START
SEEK X

@REP 11
COPY F #DIAL
@END

REPL MAIN_LOOP
LINK 800
REPL READER
LINK -1
REPL SUCCESS
SEEK 9999
MARK WRITE
COPY M F
@REP 5
COPY M T
SWIZ T 2 F
SWIZ T 1 F
@END
TEST MRD
TJMP WRITE

REPL MAIN_LOOP
COPY -1 #DIAL

;XB - LOCAL
LINK 800
@REP 8    ;SUCCESS COUNT
COPY 0 M 
@END

KILL      ;CLEANUP
GRAB 300
LINK 800
KILL
WIPE
775/79/21
A big timesaver here is to compress two digits into only one M call, by simply putting one in the tens place and the other in the ones place. That takes some extra cycles to compress/decompress but since that can be done in parallel while an M call by itself always takes two cycles it's quite fast. The other trick is to have the 'main' EXA use replication to run some steps in parallel. For instance, it makes a clone, then goes into the remote connection itself to check if it's valid. If so it LINKs back and starts copying phone numbers, otherwise the clone grabs the main phone number file and takes over. This is made possible by storing the offset of the oldest undialed number in X.

Then, in the greatest collaboration of the century, GuavaMoment combined the fixed area code solution with silentsnack's parallellism solution.

GuavaMoment posted:

code:
;XA
GRAB 300
VOID F
VOID F
VOID F
LINK 800
JUMP START

MARK READER
GRAB 200
MARK READ
SEEK 4
@REP 4
MULI F 10 X
ADDI X F M
@END
JUMP READ

MARK MAIN_LOOP
ADDI X 8 X
GRAB 300
MARK START
SEEK X
COPY 4 #DIAL
COPY 7 #DIAL
COPY 2 #DIAL
@REP 8
COPY F #DIAL
@END

REPL MAIN_LOOP
LINK 800
REPL READER
LINK -1
SEEK 9999
MARK WRITE
@REP 4
COPY M T
SWIZ T 2 F
SWIZ T 1 F
@END
NOOP ; See note 1
TEST MRD
TJMP WRITE

REPL MAIN_LOOP
COPY -1 #DIAL

;XB
COPY 334 T
MARK WAIT
SUBI T 1 T
TJMP WAIT
LINK 800
KILL
GRAB 300
WIPE
LINK 800
676/67/21. XB was changed to a simple timer to speed up an edge case in one specific test. 'note 1' is about how that NOOP can be removed if you can unroll the main READ loop, but this doesn't seem possible within the max size constraint.

Finally, silentsnack also posted a solution that only takes 23 lines of code.

silentsnack posted:

code:
GRAB 300

MARK DIAL_LOOP
COPY F X
REPL PHONE
SEEK -1
VOID F
NOOP
MARK COPY_LOOP
SEEK -9999
TEST MRD
FJMP DIAL_LOOP

COPY -1 F
SEEK 9999
COPY M F
JUMP COPY_LOOP

MARK PHONE
LINK 800
COPY X #DIAL
LINK 800
GRAB 200
MARK READ
COPY F M
JUMP READ
3879/23/272
It's much lower than what I got and that's for good reason, it uses some clever tricks. It creates a new REPL for EVERY digit, which dials that digit and then checks if there's a connection. If so, the main EXA jumps into writing mode, otherwise it tries with the next digit. This always works - if a full number can't be dialed the modem resets automatically, so as long as you have all the phone numbers in order in the file you don't need extra logic and you can just start dialing the next number immediately. The writer also doesn't bother skipping the contact's name. If the modem is idle, sending that to the modem doesn't do anything and is safe. The only special case is hanging up after a successful connection. This is established by putting a -1 at the start of the file whenever a connection has been made. It does this "too often" but that's the price for low size optimization.

The second trick is the interleaving loops. The lines between MARK COPY_LOOP and FJMP DIAL_LOOP are shared by both. If you're dialing and TEST MRD is false no connection has been made yet, dial the next digit. If it's true start copying. If you're copying and TEST MRD is true, there's another digit to copy, so continue. If it's false it's done and you can start dialing again.

A construct like that isn't really possible in modern high-level languages so I wouldn't have considered it as an option until silentsnack showed it here.

Finally, thanks to TwelveBaud for pointing out Zachtronics released two ports of the HACK*MATCH arcarde game, one is an actual NES port, the other is one of the sub-games of the new game Last Call BBS. Last Call BBS is the very last game that will be released by Zachtronics. However, not all is lost. Zach stated in an interview a week ago that he's not going to pursue his teaching career any further and might decide to go back into creating video games. Another employee, Matthew Burns, wants to continue creating games as well. It's a bit sad for Zach that his teaching career didn't work out, but I'm not going to deny I'd be happy to see more games from his hand.

Anyway, that's enough yapping. I got an LP to write right here.


=== Last Stop Snaxnet - Nuclear centrifuges ===

There's a ton of dataphones out there.
Not very powerful individually, but tied together they work somewhat decently.
I'll just be very... distributed.




A divided vote. One for "You're scaring me", two for "Won't that be slow", and three for the winning choice.

Is it going to be enough?

I'm not sure.
I'll have to try it out and see.
Let's continue.




C'mon Ember, you almost got us discovered.



Last time, Isadora told us about the Last Stop cult. I need to do something about it.

Let me see if I understood that correctly.
Last Stop, the convenience store chain, is actually a front for a cult.
They believe the world isn't real, and are in the process of creating a nuclear weapon in order to escape it.
And now you're going to attempt to take care of it yourself, instead of alerting the authorities like a normal person.




Two votes for "The authorities won't help", four for "Nothing about this is normal".

Nothing about this is normal.

True.
Well, go on, have your fun.



OST: Leave No Trace

The assignment:
- An array of five Zippe-type gas centrifuges, ZGC0 through ZGC4, are connected in a cascade configuration.
- Read each of the #ZGCX registers and determine which centrifuge currently has the highest pressure. Then disable that centrifuge's regulator by writing a value of 0 to its #POWR register. Repeat this process until all five regulators have been disabled.


Man, seeing a nuclear centrifuge control system with the logo of the local convenience store is very weird. There's even store inventory files sitting there.

Well, the assignment sounds straightforward enough, let's get started.

It's important to know that whenever you disable a centrifuge, its pressure drops to zero, and the pressure of all other centrifuges changes such that the next one to shut down is unpredictable.

As you know I like to immediately go for some optimization with my first solution if I can. I decided to go for activity once more. It's a tricky one because you can't just hop back and forth to get to the right centrifuge.

code:
LINK 800
REPL READER
LINK 798
REPL NEXT_CEN
MAKE
JUMP POWEROFFLP


MARK READER
LINK 799
MAKE
For the initial setup, I LINK into the Snaxnet inventory host, create a reader which goes to the status host and makes a file for later use, and send another EXA to the first centrifuge. That EXA makes another REPL for the next centrifuge, makes a file and jumps into the POWEROFFLP.
code:
MARK NEXT_CEN
ADDI 1 X X
LINK 800
REPL NEXT_CEN
MAKE
MARK POWEROFFLP
Skipping the reader logic for now, the NEXT_CEN loop adds one to X, links to the next centrifuge, REPLs so that every centrifuge gets occupied. Then it makes a file and also falls through into the POWEROFFLP code. The end result is that every centrifuge host has an EXA, each one holding an empty file, and X stores the number of the centrifuge it's at. The REPL from the last centrifuge will just LINK to nowhere and die.

code:
COPY M F
SEEK -1
TEST F = X
TJMP DOIT
SEEK -1
COPY F M

COPY 11 T
MARK WT
SUBI T 1 T
TJMP WT
JUMP POWEROFFLP

MARK DOIT
COPY 0 #POWR
COPY -1 M
MARK END
WIPE
Here is the trick that allows me to get a low activity score. The reader will eventually send the number of the centrifuge to disable over M, and it will be picked up by a RANDOM centrifuge EXA. It temporarily writes the value to F, and tests if the value is intended for itself. If not, it forwards it to another random EXA. Eventually it'll reach the right EXA by pure luck, triggering the DOIT jump which causes the EXA to turn off the centrifuge and send a -1 over M to tell the READER it's done. This -1 can also be picked up by a centrifuge EXA but those will just forward it. Every time a centrifuge goes down and its EXA also shuts down, there are less EXAs where the message can end up so it gets faster.
I swayed the probability a bit by adding a wait loop after receiving a wrong message so that the same EXA doesn't get the wrong message twice in a row. By trial and error, I found 11 loops to be the fastest.

Finally, the most complicated bit is the actual reader code. I have to find the highest value - and not just that, the EXA needs to remember which centrifuge it belongs to.
code:
MARK READER
LINK 799
MAKE

MARK CONTINUE
COPY #ZGC0 X
COPY #ZGC1 F
COPY #ZGC2 F
COPY #ZGC3 F
COPY #ZGC4 F
SEEK -9999

MARK TESTNEXT
TEST F > X
SEEK -1
FJMP LOWER

COPY F X
SEEK -1
MARK LOWER
VOID F
TEST EOF
FJMP TESTNEXT

TEST X = 0
TJMP END
Here is the first half. The value of the first centrifuge goes into X, the rest goes into F. Then the loop compares the value in X to the one in F. If F has the lower value it is deleted, if it is higher, the value is copied to X and also deleted from F. Once EOF is reached, X contains the highest value and F is empty - which is convenient for the next round. If X is zero that means all centrifuges have shut down and this EXA can WIPE its file and halt.

code:
TEST #ZGC0 = X
TJMP ZERO
TEST #ZGC1 = X
TJMP ONE
TEST #ZGC2 = X
TJMP TWO
TEST #ZGC3 = X
TJMP THREE
COPY 4 M
JUMP NXTREAD
MARK ZERO
COPY 0 M
JUMP NXTREAD
MARK ONE
COPY 1 M
JUMP NXTREAD
MARK TWO
COPY 2 M
JUMP NXTREAD
MARK THREE
COPY 3 M

MARK NXTREAD
COPY M X
TEST X = -1
TJMP CONTINUE
COPY X M
JUMP NXTREAD
As for finding which centrifuge it is - there are lots of ways to keep track of that but I just decided to test the value in X against every register and hardcode the numbers. It's not the prettiest code but it's hell of a lot easier than keeping track of where you are in the file while also updating it.

After sending the value, this EXA has to wait for a message that the centrifuge has powered down. If it doesn't wait, it'll read old values from the registers and get the wrong centrifuge value. Just like the other EXAs, this one might accidentally get a shutdown-a-centrifuge message that's being forwarded by one of the centrifuge EXAs. In that case it needs to forward it again. I didn't bother implementing a timer here because brute-forcing two separate timers to reduce randomness is a lot of work, and this low activity solution isn't going to be the fastest anyway.



The top percentiles sit at 89, 44, 7.



As you can see, when I start shutting down centrifuges, the remaining ones start shaking as their pressure builds up. Shutting down the last one makes the camera lose power. Looks like I've done some real damage.


Optimization time.

To optimize for speed, let's first get rid of that wonky M redirect stuff by dropping the activity requirement.
code:
LINK 800
LINK 799
MAKE

MARK CONTINUE
COPY #ZGC0 X
COPY #ZGC1 F
COPY #ZGC2 F
COPY #ZGC3 F
COPY #ZGC4 F
SEEK -9999

MARK TESTNEXT
TEST F > X
SEEK -1
FJMP LOWER

COPY F X
SEEK -1
MARK LOWER
VOID F
TEST EOF
FJMP TESTNEXT

TEST X = 0
TJMP END
REPL WRITER

TEST #ZGC1 = X
TJMP ONE
TEST #ZGC2 = X
TJMP TWO
TEST #ZGC3 = X
TJMP THREE
TEST #ZGC4 = X
TJMP FOUR
COPY 0 M
JUMP NXTREAD
MARK ONE
COPY 1 M
JUMP NXTREAD
MARK TWO
COPY 2 M
JUMP NXTREAD
MARK THREE
COPY 3 M
JUMP NXTREAD
MARK FOUR
COPY 4 M

MARK NXTREAD
COPY 8 T
MARK WT
SUBI T 1 T
TJMP WT
JUMP CONTINUE

MARK WRITER
LINK -1
LINK 798
COPY M T

MARK LINK
FJMP SHUTOFF
LINK 800
SUBI T 1 T
JUMP LINK

MARK SHUTOFF
COPY 0 #POWR

MARK END
WIPE
The code that detects the highest value is unchanged, but instead of having EXAs everywhere, the main one makes a WRITER whenever there's still a centrifuge to shut down. It then walks to the right centrifuge. I tried some alternatives, such as one permanently listening on M, or not using M at all and only making the WRITER once the centrifuge ID was determined and stored in X but those were all slightly slower. Surprisingly, this code runs at 352/63/22 which is only barely faster than my initial solution.

Clearly the slowest part is finding the highest value. So for my next change I tried to parallellize that a bit and do a kind of binary search.
code:
LINK 800
LINK 799
MAKE

MARK CONTINUE
REPL A
REPL B

COPY #ZGC4 F
SEEK -1
COPY M X
TEST X > F
TJMP COMPARE_X
SEEK -1
COPY F X


MARK COMPARE_X
COPY M F
SEEK -1
TEST F > X
FJMP SKIP_GET_F

SEEK -1
COPY F X
MARK SKIP_GET_F


TEST X = 0
TJMP END
REPL WRITER

TEST #ZGC1 = X
TJMP ONE
TEST #ZGC2 = X
TJMP TWO
TEST #ZGC3 = X
TJMP THREE
TEST #ZGC4 = X
TJMP FOUR
COPY 0 M
JUMP NXTREAD
MARK ONE
COPY 1 M
JUMP NXTREAD
MARK TWO
COPY 2 M
JUMP NXTREAD
MARK THREE
COPY 3 M
JUMP NXTREAD
MARK FOUR
COPY 4 M

MARK NXTREAD
VOID M
JUMP CONTINUE


MARK WRITER
LINK -1
LINK 798
COPY M T

MARK LINK
FJMP SHUTOFF
LINK 800
SUBI T 1 T
JUMP LINK

MARK SHUTOFF
COPY 0 M
COPY 0 #POWR

MARK A
TEST #ZGC0 < #ZGC1
TJMP ONEISBIGGER
COPY #ZGC0 M
HALT
MARK ONEISBIGGER
COPY #ZGC1 M
HALT

MARK B
TEST #ZGC2 < #ZGC3
TJMP THREEISBIGGER
COPY #ZGC2 M
HALT
MARK THREEISBIGGER
COPY #ZGC3 M

MARK END
WIPE
198/77/22
To minimize the amount of jumps, the code is ordered strangely. Each round, the main EXA creates A and B EXAs. A compares ZGC 0 and 1 and sends the largest value over M, B does this for 2 and 3. The main EXA reads ZGC 4 and compares the results from A and B to that, keeping the largest value in X to find which centrifuge it belonged to. I also noticed that I didn't really need the countdown wait anymore. It's now faster to have the poweroff EXA send a quick message on M to let the main one know it can continue.

I'm pretty sure the next big improvement would be to somehow keep track of which value is which so I don't need to check the registers twice. I couldn't figure out how to do this fast, though.


So, let's look at the low-size solution instead.

I got it down to 50 lines of code.
code:
LINK 800
LINK 799
MAKE

MARK CONTINUE
COPY 0 X
COPY #ZGC0 F
COPY #ZGC1 F
COPY #ZGC2 F
COPY #ZGC3 F
COPY #ZGC4 F
SEEK -5

MARK TESTNEXT
TEST F > X
FJMP LOWER

SEEK -1
COPY F X
MARK LOWER
TEST EOF
FJMP TESTNEXT

SEEK -5

TEST X = 0
TJMP END
REPL WRITER

MARK AGAIN
COPY 1 M
TEST F = X
FJMP AGAIN

VOID M
JUMP CONTINUE

MARK WRITER
COPY -1 X
LINK -1
LINK 798

MARK WAIT
ADDI M X X
NOOP
NOOP
TEST MRD
TJMP WAIT

COPY X T

MARK GO
FJMP POWER
LINK 800
SUBI T 1 T
JUMP GO

MARK POWER
COPY 0 #POWR
COPY 1 M

MARK END
WIPE
374/50/22. I store all 5 values in a file, then put the highest value in X. Then in the AGAIN loop I send 1 to M for each value in the file until I reach the value that matches the highest. The WRITER starts with -1 in X, so by adding M to it until the main EXA stops sending, it'll hop 0 spaces for ZGC0, 1 for ZGC1 and so on.

There are some problems with this code - for instance the two NOOPs feel rather useless but I can't just remove them because the main EXA isn't faster than that, and there's nothing useful I can do in those cycles since both registers are already in use. There's also the COPY 0 X near the top. I hoped this wouldn't be necessary. It turns out that if you diaable the highest pressure centrifuge, all others will get an even higher pressure, so comparing new values with the X from a previous round would be fine. However, I need that line for TEST X = 0 to see if I'm completely done.

If I can't remove those lines, can I at least combine some stuff?
code:
;XA

LINK 800
LINK 799
MAKE

MARK CONTINUE
COPY M X
COPY #ZGC0 F
COPY #ZGC1 F
COPY #ZGC2 F
COPY #ZGC3 F
COPY #ZGC4 F
SEEK -5

MARK TESTNEXT
TEST F > X
FJMP LOWER

SEEK -1
COPY F X
MARK LOWER
TEST EOF
FJMP TESTNEXT

SEEK -5

TEST X = 0
TJMP END
REPL WRITER

MARK SENDLINK
TEST F = X
TJMP CONTINUE
COPY 800 M
JUMP SENDLINK

MARK WRITER
LINK -1
LINK 798

MARK AGAIN
NOOP
NOOP
TEST MRD
FJMP POWER
LINK M
JUMP AGAIN

MARK POWER
COPY 0 #POWR
COPY 0 M

MARK END
WIPE

;XB
COPY 0 M
314/44/22. The main gain is combining the two loops in the WRITER EXA, to LINK as soon as it gets a message on M. By sending the link ID (800) on M, I can even just use LINK M. I had to turn the loop that sends data to M (SENDLINK in this code) upside down to prevent it sending anything to M if centrifuge zero needs to be turned off. Also, since the M that indicates the WRITER is done didn't have any useful value, I combined that with setting X back to 0 at the top. This requires one line in XB to get the program going, but since it saves several jump statements it's worth it.

My high scores are 198, 44 and 7, with size and activity sitting at the top percentile. There's a large improvement possible for cycles, and I wouldn't be surprised if you could squeeze a couple more lines out of the low-size solution too.

So, the part about the world not being real...
I think maybe they were onto something.




And that brings us to our first vote. As for next time...

More phage problems, huh?
There's a rumor that the phage originally came from a research lab...
A human-machine interface experiment that found its way into the wild.